package plaid.parser.ast.util;

import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;

class StringComparator
		implements
			Comparator<String>,
			Serializable
		{
	
	private static final long serialVersionUID = 5603928894805771653L;
	
	static public final StringComparator Default = new StringComparator();

	/**
	 * Compares two strings.
	 **/
	public int compare(
			final String pString1,
			final String pString2) {
		
		if (pString1 == pString2)
			return 0;
		
		if (pString1 == null)
			return Integer.MIN_VALUE;
		
		if (pString2 == null)
			return Integer.MAX_VALUE;
		
		if (pString1.equals(pString2))
			return 0;
		
		final int aS1_Length = pString1.length();
		final int aS2_Length = pString2.length();
		final int aMinLength = Math.min(aS1_Length, aS2_Length);
		for(int i = 0; i < aMinLength; i++) {
			char aC1 = pString1.charAt(i);
			char aC2 = pString2.charAt(i);
			
			if (aC1 != aC2)
				return aC1 - aC2;
		}
		
		final int aLenghtDiffer = aS1_Length - aS2_Length;
		return aLenghtDiffer;
	}

}

class ClassComparator
		implements
			Comparator<Class<?>>,
			Serializable
		{
	
	private static final long serialVersionUID = -7120534646583185358L;
	
	static public final ClassComparator Default = new ClassComparator();
		
	/** Compares two classes. */
	public int compare(
			final Class<?> pClass1,
			final Class<?> pClass2) {
		
		if (pClass1 == pClass2)
			return 0;
		
		if (pClass1 == null)
			return Integer.MIN_VALUE;
		
		if (pClass2 == null)
			return Integer.MAX_VALUE;
		
		final String aC1_Name      = pClass1.getCanonicalName();
		final String aC2_Name      = pClass2.getCanonicalName();
		final int    aNameCompared = StringComparator.Default.compare(aC1_Name, aC2_Name);
		return aNameCompared;
	}

}

public class ClassDiscovery {

	static private String GetClassName_afterPackageAsPath(
			final String pFileName,
			final String pPkgAsPath) {
		final String  CName                    = pFileName.substring(0, pFileName.length() - 6).replace('/', '.').replace('\\', '.');
		final String  CName_AfterPackageAsPath = CName.substring(pPkgAsPath.length());
		return CName_AfterPackageAsPath;
	}
	static private String GetClassName_ofPackageAsPath(
			final String pFileName,
			final String pPkgAsPath) {
		
		final boolean aIsClass = pFileName.endsWith(".class");
		if (!aIsClass)
			return null;

		final boolean aIsBelongToPackage = pFileName.startsWith(pPkgAsPath);
		if (!aIsBelongToPackage)
			return null;

		final String  aClassName = ClassDiscovery.GetClassName_afterPackageAsPath(pFileName, pPkgAsPath);
		return aClassName;
	}

	static private File GetPackageFile(
			final String pPkgName,
			final File   pPkgPath) {
		
		final String aPkgFileName = pPkgPath.getAbsoluteFile().toString() + '/' + pPkgName.replace('.', '/');
		final File   aPkgFile     = new File(aPkgFileName);
		
		final boolean aIsExist             = aPkgFile.exists();
		final boolean aIsDirectory         = aPkgFile.isDirectory();
		final boolean aIsExist_asDirectory = aIsExist && aIsDirectory;
		if(!aIsExist_asDirectory)
			return null;
		
		return aPkgFile;
	}
	static private boolean Check_isJarFile(final File pFile) {
		final boolean aIsJarFile = pFile.toString().endsWith(".jar");
		return aIsJarFile;
	}
	
	static private ArrayList<String> DiscoverClassNames_fromJarFile(final PkgInfo pPkgInfo) {
		
		final ArrayList<String> aClassNames = new ArrayList<String>();
		try {
			final JarFile               JF  = new JarFile(pPkgInfo.PkgPath);
			final Enumeration<JarEntry> JEs = JF.entries();
			
			while (JEs.hasMoreElements()) {
				final JarEntry aJE     = JEs.nextElement();
				final String   aJEName = aJE.getName();
				
				final String aSimpleName = GetClassName_ofPackageAsPath(aJEName, pPkgInfo.PkgAsPath);
				if (aSimpleName == null)
					continue;
				
				final String aClassName  = pPkgInfo.PkgName + '.' + aSimpleName;
				aClassNames.add(aClassName);
			}
			
			JF.close();
		} catch (IOException e) {}
		
		return aClassNames;
	}
	static private void DiscoverClassNames_fromDirectory(
			final String            pAbsolutePackagePath,
			final String            pPackageName,
			final File              pPackageFolder,
			final ArrayList<String> pClassNames) {
		final File[] aFiles = pPackageFolder.listFiles();
		for(File aFile : aFiles) {
			if (aFile.isDirectory()) {
				DiscoverClassNames_fromDirectory(pAbsolutePackagePath, pPackageName, aFile, pClassNames);
				continue;
			}
		
			final String  aFileName    = aFile.getAbsolutePath().substring(pAbsolutePackagePath.length() + 1);
			final boolean aIsClassFile = aFileName.endsWith(".class");
			if (!aIsClassFile)
				continue;

			final String aSimpleName = aFileName.substring(0, aFileName.length() - 6).replace('/', '.').replace('\\', '.');
			final String aClassName  = pPackageName + '.' + aSimpleName;
			pClassNames.add(aClassName);
		}
	}
	static private ArrayList<String> DiscoverClassNames_fromDirectory(PkgInfo pPkgInfo) {

		final ArrayList<String> aClassNames = new ArrayList<String>();
		final File aPkgFile = ClassDiscovery.GetPackageFile(pPkgInfo.PkgName, pPkgInfo.PkgPath);
		if (aPkgFile == null)
			return aClassNames;
		
		DiscoverClassNames_fromDirectory(aPkgFile.getAbsolutePath(), pPkgInfo.PkgName, aPkgFile, aClassNames);
		return aClassNames;
	}
	
	static public class PkgInfo {
		PkgInfo(
				final File   pPkgPath,
				final String pPkgName,
				final String pPkgAsPath) {
			
			this.PkgPath   = pPkgPath;
			this.PkgName   = pPkgName;
			this.PkgAsPath = pPkgAsPath;
		}
		final File   PkgPath;
		final String PkgName;
		final String PkgAsPath;
	}
	static public PkgInfo GetPackageInfoOf(Class<?> pClass) {
		File   aPkgPath   = null;
		String aPkgName   = null;
		String aPkgAsPath = null;
		
		try {
			aPkgPath   = new File(pClass.getProtectionDomain().getCodeSource().getLocation().toURI());
			aPkgName   = pClass.getPackage().getName();
			aPkgAsPath = aPkgName.replace('.', '/') + '/';
		}
		catch (Throwable e) { }

		if (aPkgPath == null)
			return null;
		
		final PkgInfo aPkgInfo = new PkgInfo(aPkgPath, aPkgName, aPkgAsPath);
		return aPkgInfo;
	}
	static public ArrayList<String> DiscoverClassNames_inPackage(final PkgInfo pPkgInfo) {

		if (pPkgInfo == null)
			return null;

		ArrayList<String> aClassNames = new ArrayList<String>();
		if (pPkgInfo.PkgPath.isDirectory()) {
			
			aClassNames = ClassDiscovery.DiscoverClassNames_fromDirectory(pPkgInfo);
			
		} else if(pPkgInfo.PkgPath.isFile()) {
			boolean aIsJarFile = ClassDiscovery.Check_isJarFile(pPkgInfo.PkgPath);
			if (!aIsJarFile)
				return null;
			
			aClassNames = ClassDiscovery.DiscoverClassNames_fromJarFile(pPkgInfo);
		}
		
		return aClassNames;
	} 
	
	/**
	 * Returns an array of class in the same package as the the SeedClass
	 * 
	 * @param pFilterName     - Regular expression to match the desired classes' name (nullif no filtering needed)
	 * @param pFilterClass   - The super class of the desired classes (null if no filtering needed)
	 * 
	 * @return                - The array of matched classes, null if there is a problem.
	 * 
	 * @author The rest       - Nawaman  http://nawaman.net
	 * @author Package as Dir - Jon Peck http://jonpeck.com
	 *                              (adapted from http://www.javaworld.com/javaworld/javatips/jw-javatip113.html)
	 */
	@SuppressWarnings("unchecked")
	static <T> Class<? extends T>[] DiscoverClasses(
			final Class<?> pSeedClass,
			final String   pFilterName,
			final Class<T> pFilterClass) {

		final Pattern aClsNamePattern = (pFilterName == null) ? null : Pattern.compile(pFilterName);
		
		PkgInfo aPkgInfo = null;

		try { aPkgInfo = GetPackageInfoOf(pSeedClass); }
		catch (Throwable e) {}

		if (aPkgInfo == null)
			return null;
		
		final ArrayList<String> aClassNames = DiscoverClassNames_inPackage(aPkgInfo);
		
		if (aClassNames == null)
			return null;
		
		if (aClassNames.size() == 0)
			return null;
		
		final ArrayList<Class<?>> aClasses = new ArrayList<Class<?>>();
		for(final String aClassName : aClassNames) {
			
			if ((aClsNamePattern != null) && !aClsNamePattern.matcher(aClassName).matches())
				continue;

			// Get the class and filter it
			Class<?> aClass = null;
			try {
				aClass = Class.forName(aClassName);
			} catch (ClassNotFoundException e) {
				continue;
			} catch (NoClassDefFoundError e) {
				continue;
			}
			
			if((pFilterClass != null) && !pFilterClass.isAssignableFrom(aClass))
				continue;

			if (pFilterClass != null) {
				if (!pFilterClass.isAssignableFrom(aClass))
					continue;
					
				aClasses.add(aClass.asSubclass(pFilterClass));
			} else {
				aClasses.add(aClass);
			}
		}

		Collections.sort(aClasses, ClassComparator.Default);
		Class<? extends T>[] aClassesArray = aClasses.toArray((Class<? extends T>[]) (new Class[aClasses.size()]));

		return aClassesArray;
	}
	
	static public void main(String ... pArgs) {
		Class<?> aSeedClass = ClassDiscovery.class;
		try { aSeedClass = Class.forName(pArgs[0]); }
		catch(Exception E) {}
		
		if (aSeedClass == null)
			aSeedClass = ClassDiscovery.class;
		
		final Class<?>[] aClasses =	DiscoverClasses(aSeedClass, null, null);
		
		System.out.println("[");
		if (aClasses != null) {
			for (Class<?> aClass : aClasses)
				System.out.println("\t" + aClass);
		}
		System.out.println("]");
	}

}
